﻿using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Quickly.Spa.Shared;
using System.Diagnostics.CodeAnalysis;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Transforms;

namespace Quickly.SpaProxy.ReverseProxy;

internal sealed class SpaReverseProxyConfigProvider : IProxyConfigProvider, IDisposable
{
    private readonly object _lock = new object();
    private readonly ILogger<SpaReverseProxyConfigProvider> _logger;

    private IConfiguration _configuration { get; }
    private SpaReverseProxyConfig? _snapshot;
    private CancellationTokenSource? _changeToken;
    private IDisposable? _subscription;

    private bool _disposed;

    public SpaReverseProxyConfigProvider(ILogger<SpaReverseProxyConfigProvider> logger, IConfiguration configuration)
    {
        this._logger = logger;
        this._configuration = configuration;
    }

    public IProxyConfig GetConfig()
    {
        if (_snapshot is null)
        {
            _subscription = ChangeToken.OnChange(_configuration.GetReloadToken, UpdateSnapShot);
            UpdateSnapShot();
        }

        return _snapshot;
    }

    [MemberNotNull(nameof(_snapshot))]
    private void UpdateSnapShot()
    {
        lock (_lock)
        {
            Log.LoadData(_logger);

            SpaReverseProxyConfig newSnapShot;

            try
            {
                newSnapShot = new SpaReverseProxyConfig();
                var options = new SpaDevelopmentServerOptions();
                _configuration.Bind(options);

                foreach (var server in options)
                {
                    newSnapShot.Clusters.Add(CreateCluster(server.Key, server.Value));
                    newSnapShot.Routes.AddRange(CreateRoute(server.Key, server.Value));
                }
            }
            catch (Exception ex)
            {
                Log.ConfigurationDataConversionFailed(_logger, ex);

                if (_snapshot is null)
                {
                    throw;
                }

                return;
            }

            var oldToken = _changeToken;
            _changeToken = new CancellationTokenSource();
            newSnapShot.ChangeToken = new CancellationChangeToken(_changeToken.Token);
            _snapshot = newSnapShot;

            try
            {
                oldToken?.Cancel(throwOnFirstException: false);
            }
            catch (Exception ex)
            {
                Log.ErrorSignalingChange(_logger, ex);
            }
        }
    }

    private IEnumerable<RouteConfig> CreateRoute(string key, SpaDevelopmentServerOption value)
    {
        var redirectUrl = value.GetRedirectUrl();

        if (redirectUrl.StartsWith('/') || string.IsNullOrWhiteSpace(redirectUrl))
        {
            if (value.MatchPaths != null && value.MatchPaths.Length > 0)
            {
                var index = 0;
                foreach (var path in value.MatchPaths)
                {
                    var pathRoute = new RouteConfig
                    {
                        RouteId = $"{key}_{index}",
                        ClusterId = key,
                        CorsPolicy = "DevAllowAnyOrigin",
                        Match = new RouteMatch
                        {
                            Path = path
                        }
                    };

                    pathRoute = pathRoute.WithTransformPathRemovePrefix(redirectUrl);

                    yield return pathRoute;
                }
            }


            //var defaultRootConfig = new RouteConfig
            //{
            //    RouteId = $"{key}_default",
            //    ClusterId = key,
            //    CorsPolicy = "DevAllowAnyOrigin",
            //    Match = new RouteMatch
            //    {
            //        Path = Path.Combine(redirectUrl.StartsWith('/') ? $"{redirectUrl.TrimEnd('/')}/{{**catch-all}}" : "/{**catch-all}")
            //    }
            //};

            //defaultRootConfig = defaultRootConfig.WithTransformPathRemovePrefix(redirectUrl);

            //yield return defaultRootConfig;
        }

        yield break;
    }

    private ClusterConfig CreateCluster(string key, SpaDevelopmentServerOption value)
    {
        var config = new ClusterConfig
        {
            ClusterId = key,
            Destinations = new Dictionary<string, DestinationConfig>
            {
                ["default"] = new DestinationConfig
                {
                    Address = value.ServerUrl,
                }
            }
        };

        return config;
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: 释放托管状态(托管对象)
            }

            // TODO: 释放未托管的资源(未托管的对象)并重写终结器
            _subscription?.Dispose();
            _changeToken?.Dispose();

            // TODO: 将大型字段设置为 null
            _disposed = true;
        }
    }

    // // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
    // ~SpaReverseProxyConfigProvider()
    // {
    //     // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }


    private static class Log
    {
        private static readonly Action<ILogger, Exception> _errorSignalingChange = LoggerMessage.Define(
            LogLevel.Error,
            EventIds.ErrorSignalingChange,
            "An exception was thrown from the change notification.");

        private static readonly Action<ILogger, Exception?> _loadData = LoggerMessage.Define(
            LogLevel.Information,
            EventIds.LoadData,
            "Loading proxy data from spa proxy config.");


        private static readonly Action<ILogger, Exception> _configurationDataConversionFailed = LoggerMessage.Define(
            LogLevel.Error,
            EventIds.ConfigurationDataConversionFailed,
            "Configuration data conversion failed.");

        public static void ErrorSignalingChange(ILogger logger, Exception exception)
        {
            _errorSignalingChange(logger, exception);
        }

        public static void LoadData(ILogger logger)
        {
            _loadData(logger, null);
        }

        public static void ConfigurationDataConversionFailed(ILogger logger, Exception exception)
        {
            _configurationDataConversionFailed(logger, exception);
        }
    }
}
